import React from 'react';
import BpmnModeler from 'bpmn-js/lib/Modeler';
import {
  Layout, message, Modal,
} from 'antd';
import {
  keys, omit, find, findIndex, mapValues,
} from 'lodash/fp';
import { parseXML, filterTaskInstances } from 'utils/parser';
import { listService } from 'services/microservice';
import 'assets/css/bpmn.css';
import 'bpmn-js/dist/assets/diagram-js.css';
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css';
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css';
import BpmnExtension from 'assets/BpmnExtension.json';

import { comparisonOpsNames } from './constants';
import TaskForm from './taskForm/Form';
import ExpressionForm from './expressionForm/Form';
import FlowForm from './flowForm/Form';

const { Content } = Layout;
const contentStyle = {
  background: '#fff',
  padding: 24,
  margin: 0,
  minHeight: 280,
};

/**
 * Customized BPMN Editor with extended task, exclusive gateway & sequence flows.
 * Use refs to this editor to access methods like init, saveXML & load.
 */
export default class BpmnEditor extends React.Component {
  constructor(props) {
    super(props);
    this.modeler = null;
    this.chosenGatewayId = null;
    this.chosenFlowId = null;
    this.state = {
      visible: false,
      chosenTaskId: null,
      services: [],
      taskOutput: {},
      gatewayModalVisible: false,
      flowModalVisible: false,
      tasks: {},
    };
  }

  showModal = () => {
    this.setState({
      visible: true,
    });
  }

  closeModal = () => {
    this.setState({
      visible: false,
    });
  }

  showGatewayModal = () => {
    this.setState({
      gatewayModalVisible: true,
    });
  }

  closeGatewayModal = () => {
    this.setState({
      gatewayModalVisible: false,
    });
  }

  showFlowModal = () => {
    this.setState({
      flowModalVisible: true,
    });
  }

  closeFlowModal = () => {
    this.setState({
      flowModalVisible: false,
    });
  }

  init = (xml) => {
    // create modeler
    this.modeler = new BpmnModeler({
      container: '#canvas',
      moddleExtensions: {
        camunda: BpmnExtension,
      },
    });
    // import xml
    this.modeler.importXML(xml, (err) => {
      if (err) {
        message.error('error rendering', err);
      } else {
        const canvas = this.modeler.get('canvas');
        // zoom to fit full viewport
        canvas.zoom('fit-viewport');
      }
    });

    const eventBus = this.modeler.get('eventBus');
    const directEditing = this.modeler.get('directEditing');

    const self = this;
    const showModalForElement = (element) => {
      if (element.type === 'bpmn:Task') {
        self.setState({ chosenTaskId: element.id });
        self.showModal();
      } else if (element.type === 'bpmn:ExclusiveGateway') {
        self.chosenGatewayId = element.id;
        self.showGatewayModal();
      } else if (element.type === 'bpmn:SequenceFlow' && element.source && element.source.type === 'bpmn:ExclusiveGateway') {
        if (element.source.outgoing.length > 2) {
          message.error('异或网关只能有两条分支!'); // only support 'true' or 'false' choices for now
          return;
        }
        self.chosenFlowId = element.id;
        self.showFlowModal();
      }
    };

    // console.log(eventBus);
    // const events = keys(eventBus._listeners);
    // events.forEach((event) => {
    //   eventBus.on(event, (e) => {
    //     console.log(e);
    //   });
    // });

    // note: there is no way we can prevent autoPlace to proceed according to the source code
    //       of bpmn-js. we can only alert the user about this.
    eventBus.on('autoPlace.start', 1500, (e) => {
      const { source } = e;
      if (source.type === 'bpmn:ExclusiveGateway' && source.outgoing.length >= 2) {
        message.error('异或网关只能有两条分支!'); // only support 'true' or 'false' choices for now
        return false;
      }
      return true;
    });

    // on create end, show modal for it
    eventBus.on('create.end', 250, (e) => {
      directEditing.cancel();
      if (e.elements && e.elements.length === 1) { // has only created one element
        const element = e.elements[0];
        showModalForElement(element);
      }
    });

    // on auto place end, cancel direct editing and show modal
    eventBus.on('autoPlace.end', (e) => {
      const { shape } = e;
      showModalForElement(shape);
      directEditing.cancel();
      e.stopPropagation();
    });

    // on double click show modal
    eventBus.on('element.dblclick', 1500, (e) => {
      // e.element = the model element
      const { element } = e;
      showModalForElement(element);
      e.stopPropagation();
    });

    // on task removed, remove corresponding output
    eventBus.on('shape.removed', (e) => {
      const { element } = e;
      if (element.type === 'bpmn:Task') {
        // delete task output
        const { taskOutput } = self.state;
        self.setState({
          taskOutput: omit([element.id], taskOutput),
        });
      }
    });

    // list of available service for tasks
    listService().then((data) => {
      const serviceList = parseXML(xml);
      const tasks = serviceList.reduce(
        (prev, cur) => {
          const service = findIndex({ name: cur.serviceName }, data);
          const startInterface = findIndex({ name: cur.startInterface }, data[service].apis);
          const detectInterface = findIndex({ name: cur.detectInterface }, data[service].apis);
          const input = cur.input ? mapValues((value) => value.split('.'), cur.input) : null;
          return {
            ...prev,
            [cur.taskId]: {
              service,
              startInterface,
              detectInterface,
              params: {
                args: cur.params,
                input,
              },
            },
          };
        },
        {},
      );
      const taskOutput = serviceList.reduce(
        (prev, cur) => {
          const serviceObj = find({ name: cur.serviceName }, data);
          const detectInterfaceObj = find({ name: cur.detectInterface }, serviceObj.apis);
          return {
            ...prev,
            [cur.taskId]: keys(detectInterfaceObj.responses[0].schema.properties.data.properties),
          };
        },
        {},
      );
      this.setState({
        services: data,
        tasks,
        taskOutput,
      });
    });
  }

  saveXML = (callback) => {
    this.modeler.saveXML({ format: true }, (err, xml) => {
      const filtered = filterTaskInstances(xml);
      callback(err, filtered);
    });
  }

  load = (xml) => {
    this.modeler.importXML(xml, (err) => {
      if (err) {
        message.error('error rendering', err);
      } else {
        const canvas = this.modeler.get('canvas');
        // zoom to fit full viewport
        canvas.zoom('fit-viewport');
      }
    });
  }

  /**
   * update task element in BPMN
   * @param {*} params task parameters, including args(literals) & input (from other tasks)
   * @param {*} service name of the service
   * @param {*} interfaceName name of the start interface
   * @param {*} startInterface start interface url
   * @param {*} detectInterface detect interface url
   */
  updateBPMN = (taskId, params, service, interfaceName, startInterface, detectInterface) => {
    // retrieve necessary objects from modeler
    const elementRegistry = this.modeler.get('elementRegistry');
    const modeling = this.modeler.get('modeling');
    const moddle = this.modeler.get('moddle');

    // create properties
    const chosenElement = elementRegistry.get(taskId);
    const extensionElements = moddle.create('bpmn:ExtensionElements');
    const propertyService = moddle.create('camunda:Property');
    propertyService.name = 'serviceName';
    const propertyName = moddle.create('camunda:Property');
    propertyName.name = 'interfaceName';
    const propertyStart = moddle.create('camunda:Property');
    propertyStart.name = 'startInterface';
    const propertyDetect = moddle.create('camunda:Property');
    propertyDetect.name = 'detectInterface';
    const propertyArgs = moddle.create('camunda:Property');
    propertyArgs.name = 'arguments';
    const propertyInputs = moddle.create('camunda:Property');
    propertyInputs.name = 'input';

    // set values of properties
    propertyService.value = service;
    propertyName.value = interfaceName;
    propertyStart.value = startInterface;
    propertyDetect.value = detectInterface;
    if (params) {
      keys(params.args || {}).forEach((key) => {
        const temp = moddle.create('camunda:Property');
        temp.name = key;
        temp.value = params.args[key];
        propertyArgs.get('subproperty').push(temp);
      });
      keys(params.input || {}).forEach((key) => {
        const temp = moddle.create('camunda:Property');
        temp.name = key;
        temp.value = params.input[key];
        propertyInputs.get('subproperty').push(temp);
      });
    }

    const properties = moddle.create('camunda:Properties');
    properties.get('subproperty').push(propertyService);
    properties.get('subproperty').push(propertyName);
    properties.get('subproperty').push(propertyStart);
    properties.get('subproperty').push(propertyDetect);
    properties.get('subproperty').push(propertyArgs);
    properties.get('subproperty').push(propertyInputs);
    extensionElements.get('values').push(properties);

    const taskName = `${interfaceName}\nid: ${taskId}`;
    // do update
    modeling.updateProperties(chosenElement, {
      name: taskName,
      extensionElements,
    });
  }

  setTaskState = (taskId, taskObj) => {
    const { tasks } = this.state;
    this.setState({
      tasks: {
        ...tasks,
        [taskId]: taskObj,
      },
    });
  }

  addExpressionToGateway = (left, middle, right) => {
    // retrieve necessary objects from modeler
    const elementRegistry = this.modeler.get('elementRegistry');
    const modeling = this.modeler.get('modeling');
    const moddle = this.modeler.get('moddle');

    // create properties
    const chosenElement = elementRegistry.get(this.chosenGatewayId);
    const extensionElements = moddle.create('bpmn:ExtensionElements');
    const propertyJudgement = moddle.create('camunda:Property');
    propertyJudgement.name = 'judgement';
    const propertyLeft = moddle.create('camunda:Property');
    propertyLeft.name = 'left';
    const propertyMiddle = moddle.create('camunda:Property');
    propertyMiddle.name = 'middle';
    const propertyRight = moddle.create('camunda:Property');
    propertyRight.name = 'right';

    // set values of properties
    propertyLeft.value = left;
    propertyMiddle.value = middle;
    propertyRight.value = right;
    propertyJudgement.get('subproperty').push(propertyLeft, propertyMiddle, propertyRight);

    const properties = moddle.create('camunda:Properties');
    properties.get('subproperty').push(propertyJudgement);
    extensionElements.get('values').push(properties);

    // values of left & right be like `ref: ${TaskId.Output}` or `lit: ${literal}`
    const expression = `${left.substring(5)} ${comparisonOpsNames[middle]} ${right.substring(5)}`;
    // do update
    modeling.updateProperties(chosenElement, {
      name: expression,
      extensionElements,
    });
  }

  /**
   * add task output to state
   */
  addTaskOutput = (taskId, output) => {
    const { taskOutput } = this.state;
    this.setState({
      taskOutput: { ...taskOutput, [taskId]: output },
    });
  }

  setSequenceFlowControl = (value) => {
    // retrieve necessary objects from modeler
    const elementRegistry = this.modeler.get('elementRegistry');
    const modeling = this.modeler.get('modeling');
    const flowId = this.chosenFlowId;

    // create properties
    const chosenElement = elementRegistry.get(flowId);
    modeling.updateProperties(chosenElement, { name: value });
    if (chosenElement.source.outgoing.length === 2) { // has peer sequence flow
      const contrast = value === 'yes' ? 'no' : 'yes';
      const contrastElm = find((flow) => flow.id !== flowId)(chosenElement.source.outgoing);
      modeling.updateProperties(contrastElm, { name: contrast });
    }
  }

  render() {
    const {
      chosenTaskId, services, visible, taskOutput, gatewayModalVisible, flowModalVisible, tasks,
    } = this.state;
    return (
      <>
        <Content style={contentStyle}>
          <div id="canvas" style={{ height: '500px' }} />
        </Content>
        <Modal
          width={600}
          title="流程任务设置"
          visible={visible}
          footer={null}
          onCancel={this.closeModal}
          destroyOnClose
        >
          <TaskForm
            taskId={chosenTaskId}
            services={services}
            closeModal={this.closeModal}
            updateBPMN={this.updateBPMN}
            taskOutput={taskOutput}
            addTaskOutput={this.addTaskOutput}
            setTaskState={this.setTaskState}
            taskObj={tasks[chosenTaskId]}
          />
        </Modal>
        <Modal
          width={600}
          title="异或网关表达式设置"
          visible={gatewayModalVisible}
          footer={null}
          onCancel={this.closeGatewayModal}
          destroyOnClose
        >
          <ExpressionForm
            closeModal={this.closeGatewayModal}
            updateBPMN={this.addExpressionToGateway}
            taskOutput={taskOutput}
          />
        </Modal>
        <Modal
          width={600}
          title="流程网关设置"
          visible={flowModalVisible}
          footer={null}
          onCancel={this.closeFlowModal}
          destroyOnClose
        >
          <FlowForm
            closeModal={this.closeFlowModal}
            updateBPMN={this.setSequenceFlowControl}
          />
        </Modal>
        {/* <Form layout="inline">
          <Form.Item>
            <Input
              ref={(input) => { this.inputRef = input; }}
              prefix={<Icon type="file" style={{ color: 'rgba(0,0,0,.25)' }} />}
              placeholder="流程名称"
            />
          </Form.Item>
          <Form.Item>
            <Button type="default" htmlType="submit" onClick={this.uploadBPMN}>
              创建流程
            </Button>
          </Form.Item>
        </Form> */}
      </>
    );
  }
}
